Skip to content

feat: expose evaluated scenario state on scenario binary_sensors - #171

Open
lollox80 wants to merge 6 commits into
rhizomatics:mainfrom
lollox80:feat/scenario-state
Open

feat: expose evaluated scenario state on scenario binary_sensors#171
lollox80 wants to merge 6 commits into
rhizomatics:mainfrom
lollox80:feat/scenario-state

Conversation

@lollox80

Copy link
Copy Markdown
Collaborator

Motivation

expose_entities() already creates binary_sensor.supernotify_scenario_ for every scenario, but the state is hard-coded to unknown and never refreshed — unlike the delivery/transport/recipient entities, which report on/off. Users building dashboards (or automations) around "which scenarios are active right now" currently have to poll supernotify.enquire_active_scenarios. This maps onto the "Fourth" phase of the config flow roadmap (scenarios: view / enable-disable) and is independent of the config flow itself.

What it does
Evaluates each scenario's conditions with neutral variables — current occupancy and PRIORITY_MEDIUM, the same basis as enquire_active_scenarios() — and publishes on/off.
Refreshes on a 1-minute timer (time/date driven scenarios, and any dependency not captured by entity extraction) and immediately on state changes of the entities referenced by the conditions, extracted with condition.async_extract_entities. Pure in-memory evaluation over cached states, no I/O.
Scenarios whose conditions reference no entity (priority-only, or triggered through applied_scenarios like a manual emergency) stay unknown: their state is undefined outside of a notification, and saying so is more honest than guessing.
Adds HomeAssistantAPI.subscribe_interval(seconds, callback) so the timer is owned by hass_api and torn down with the other subscriptions on disconnect.
Attributes are unchanged (enabled, alias, …); enabled stays an attribute rather than becoming the state.
Design note

From mapping my own 25 scenarios: 1 is a manual switch, 17 self-activate on priority/time/occupancy, 7 are driven by existing helpers. Exposing the evaluated state read-only (with helpers referenced as inputs) keeps that automation intact, whereas modelling "scenario = helper switch" would break most of them. Happy to discuss the semantics (evaluated vs enabled) if you see it differently.

Files

notify.py (+68), hass_api.py (new subscribe_interval), CHANGELOG.md, tests/components/supernotify/test_scenario_state.py (4 tests), hass_setup_lib.py / conftest.py (expose loop on the HA mock so timers can be registered in unit tests).

Testing

Unit: 4 new tests + full suite green. Real HA: (compilare: after deploy, binary_sensor.supernotify_scenario_morning/night/dnd_globale report on/off and flip on helper changes; emergency/critical_panic stay unknown).

lollox80 and others added 4 commits August 26, 2026 01:25
`expose_entities()` already creates `binary_sensor.supernotify_scenario_<name>`
but with `state=STATE_UNKNOWN` hard-coded and never refreshed.

- evaluate each scenario's conditions with neutral variables (current
  occupancy, PRIORITY_MEDIUM - the same basis as enquire_active_scenarios)
  and publish on/off
- refresh every minute (time/date driven scenarios) and on state changes of
  the entities extracted from the conditions via
  `condition.async_extract_entities`
- scenarios whose conditions reference no entity (priority-only, or
  triggered through applied_scenarios) stay `unknown`, which is the honest
  answer for them
- new `HomeAssistantAPI.subscribe_interval()` helper so the timer is owned
  by hass_api and torn down with the other subscriptions
- test double: expose `loop` on MockableHomeAssistant so timers can be
  registered in unit tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HydbFBwYt3HV83xQ4UzdjV
@jeyrb

jeyrb commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

I think this needs a config switch, globally, perhaps per scenario, since it has potential to increase compute a lot, including some folk running this on a tiny SBC

I had thought that scenario should be broken out to a separate plugin, since the idea of switchable fragments of config could be more useful, but its a lot of work to pull apart, especially if no concrete second case (though maybe I'm overlooking my own AutoArm). Having scenarios be properly event driven, reactive scenarios, would be sort of thing that plugin would be good at

lollox80 and others added 2 commits September 3, 2026 00:45
Evaluating scenario conditions costs whatever the conditions cost, so:

- a state change now re-evaluates only the scenarios that depend on the entity
  that changed, using the index already collected for the subscriptions, rather
  than the whole registry on every event
- scenario_state.enabled: false subscribes to nothing and starts no timer
- scenario_state.refresh_interval tunes the sweep, 0 drops it while keeping the
  reactive path
- expose_state: false keeps an individual expensive scenario out of it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmMatiNzpXzZmrwWEtCT3x
@lollox80

lollox80 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Fair concern, and it turns out the numbers back it — but they point at the fan-out before the switch.

I benchmarked the refresh on the PR branch (Python 3.13, desktop CPU, 20 runs averaged), evaluating every scenario in the registry the way async_refresh_scenario_states does:

scenarios conditions full refresh
5 state / simple template / time window 0.40 ms
25 state / simple template / time window 1.58 ms
50 state / simple template / time window 1.61 ms
25 one realistic template each (states.binary_sensor | selectattr(...) | count, 200 entities in the state machine) 33.96 ms

So the cost is driven by what the conditions do, not by how many scenarios there are: a 20x spread between simple conditions and the kind of template people actually write for "any window open" or a DND window. On an SBC that is a few hundred milliseconds, and that is per refresh.

Which is the real problem: the refresh currently re-evaluates every scenario on every state change of any watched entity. One motion sensor flipping re-runs all 25. The mapping needed to avoid that is already built in this PR — _collect_scenario_condition_entities() gives entity → scenarios — it just isn't used for dispatch. Evaluating only the scenarios that depend on the entity that changed turns the common case from "all of them" into "one or two", which is a bigger win than any flag, and it makes the 60-second sweep the only place where everything is evaluated together.

That, and the switch you asked for, are now pushed onto this branch:

  • a state change now re-evaluates only the scenarios that depend on the entity that changed, using the index the PR was already building for its subscriptions;
  • scenario_state: {enabled: false} subscribes to nothing and starts no timer — states stay STATE_UNKNOWN exactly as today, so nothing is paid by anyone who doesn't want the feature;
  • scenario_state: {refresh_interval: <seconds>} tunes the sweep, since 60 s was a guess — someone on a Pi Zero may want 300, and 0 keeps the reactive path with no sweep at all;
  • expose_state: false on an individual scenario keeps one expensive template out of it without turning the whole thing off.

Seven new tests cover the dispatch and the switch, including that an unrelated entity changing evaluates nothing at all. Full suite 1047 passing, ruff check, ruff format --check and mypy clean on Python 3.13, and the docs page has the new options.

I put the global option in CONFIG_ENTRY_SCHEMA, next to housekeeping and dupe_check, since it is runtime behaviour rather than notification config — say the word if you would rather have it in the YAML supernotify: block and I will move it.

On the plugin idea: worth saying that the entity extraction in this PR is the same thing reactive scenarios would need — knowing which entities a scenario actually depends on is the prerequisite for waking it on an event instead of sweeping it on a timer. So even if scenarios do get pulled out later, that part isn't wasted work.

If any of the naming or the defaults isn't what you had in mind, it is all in one commit and easy to reshape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants